import time
from typing import List, Tuple, Any, Dict
import random

from fuzzer.GreyBoxFuzzer import GreyBoxFuzzer
from schedule.FairSchedule import FairSchedule
from runner.FunctionCoverageRunner import FunctionCoverageRunner
from runner.Runner import Runner
from utils.Mutator import Mutator
from utils.Seed import Seed


class FairFuzzer(GreyBoxFuzzer):

    def __init__(
        self,
        seeds: List[str],
        threshold: int,
        is_print: bool = True,
    ):
        schedule = FairSchedule(threshold)
        super().__init__(seeds, schedule, False)
        self.covered_paths = set()  # 覆盖的路径集合
        self.last_new_path_time = self.start_time
        self.path_frequency: Dict[int, int] = {}  # {path_id: frequency}
        self.seed_mask: Dict[str, list] = {}  # {seed.data: mask_list}
        self.seed_selected: Seed = None
        self.flag = False
        self.mutator = Mutator()
        schedule.bind_frequency(self.path_frequency)

        if is_print:
            print(
                """
┌───────────────────────┬───────────────────────┬───────────────────────┬───────────────────┬───────────────────┬────────────────┬───────────────────┐
│        Run Time       │     Last New Path     │    Last Uniq Crash    │    Total Execs    │    Total Paths    │  Uniq Crashes  │   Covered Lines   │
├───────────────────────┼───────────────────────┼───────────────────────┼───────────────────┼───────────────────┼────────────────┼───────────────────┤"""
            )

    def print_stats(self):
        def format_seconds(seconds):
            hours = int(seconds) // 3600
            minutes = int(seconds % 3600) // 60
            remaining_seconds = int(seconds) % 60
            return f"{hours:02d}:{minutes:02d}:{remaining_seconds:02d}"

        template = """│{runtime}│{path_time}│{crash_time}│{total_exec}│{total_path}│{uniq_crash}│{covered_line}│
├───────────────────────┼───────────────────────┼───────────────────────┼───────────────────┼───────────────────┼────────────────┼───────────────────┤"""
        template = template.format(
            runtime=format_seconds(time.time() - self.start_time).center(23),
            path_time=format_seconds(self.last_new_path_time - self.start_time).center(
                23
            ),
            crash_time=format_seconds(self.last_crash_time - self.start_time).center(
                23
            ),
            total_exec=str(self.total_execs).center(19),
            total_path=str(len(self.covered_paths)).center(19),
            uniq_crash=str(len(set(self.crash_map.values()))).center(16),
            covered_line=str(len(self.covered_line)).center(19),
        )
        print(template)

    def copy_str(self, seed_str: str, candidate: str, mask: list[int]):
        result = list(candidate)
        for i in range(len(mask)):
            if mask[i] == 1:
                if i < len(candidate):
                    result[i] = seed_str[i]
                else:
                    break
        return "".join(result)

    def create_candidate(self) -> str:
        """Returns an input generated by fuzzing a seed in the population"""
        seed = self.schedule.choose(self.population)

        candidate = seed.data
        # 判断是否已经有完整mask
        if (i := len(self.seed_mask.get(candidate, []))) < len(candidate):
            if i == 0:
                self.seed_mask[candidate] = []
            self.seed_selected = seed
            self.flag = True
            # 利用简单的删除判断重要性
            candidate = candidate[0:i] + candidate[i + 1 :]
        else:
            # Stacking: Apply multiple mutations to generate the candidate
            trials = min(len(candidate), 1 << random.randint(1, 5))
            for i in range(trials):
                candidate = self.mutator.mutate(candidate)
            if random.random() < 0.75:
                # 将seed关键部分黏贴回来
                candidate = self.copy_str(
                    seed.data, candidate, self.seed_mask[seed.data]
                )
        return candidate

    def run(self, runner: FunctionCoverageRunner) -> Tuple[Any, str]:
        """执行测试并更新路径统计"""
        result, outcome = super().run(runner)
        # 在测试过程中完整mask
        if self.flag:
            self.flag = False
            path_id = hash(frozenset(runner.coverage()))
            seed_path_id = hash(frozenset(self.seed_selected.coverage))
            if path_id != seed_path_id:
                self.seed_mask[self.seed_selected.data].append(0)
            else:
                self.seed_mask[self.seed_selected.data].append(1)

        # 更新路径频率
        if outcome == Runner.PASS:
            path_id = hash(frozenset(runner.coverage()))
            self.path_frequency[path_id] = self.path_frequency.get(path_id, 0) + 1

        # 处理新路径
        if outcome == Runner.PASS and path_id not in self.covered_paths:
            self.covered_paths.add(path_id)
            self.last_new_path_time = time.time()

        return result, outcome
